/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.ImportUUIDBehavior;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import offset.nodes.Constants;
import offset.nodes.client.model.SimpleNamespaceRegistry;
import offset.nodes.maven.Registration;
import offset.nodes.server.config.NodesConfig;
import offset.nodes.server.model.RepositoryExtensionsManager;
import org.apache.jackrabbit.core.TransientRepository;
import org.apache.jackrabbit.rmi.remote.RemoteRepository;
import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory;
import org.apache.jackrabbit.rmi.server.ServerAdapterFactory;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.PlugIn;
import org.apache.struts.config.ModuleConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Walter Lütgenau
 */
public class RepositoryPlugIn implements PlugIn {

    private static Logger logger = LoggerFactory.getLogger(RepositoryPlugIn.class);
    private static Logger startup = LoggerFactory.getLogger("offset.nodes.startup");
    static Repository repository = null;
    Session initSession = null;
    private static final String REPOSITORY_HOME = "NODES_REPOSITORY_HOME";
    private static final String REPOSITORY_ZIP = "/repository.zip";
    private static final String REPOSITORY_CONTENT_ZIP = "/repositoryContent.zip";
    private static final String REPOSITORY_CONFIG_NAME = "repository.xml";
    private static final String APPLICATION_NAME = "nodes";
    private static final String REPOSITORY_BASE = "schema-1.0";
    private static final String TOMCAT_HOME = "catalina.home";

    /** Creates a new instance of RepositoryPlugin */
    public RepositoryPlugIn() {
    }

    public static Repository getRepository() {
        return repository;
    }

    class RepositoryInfo {

        Repository repository;
        boolean newRepository;

        public RepositoryInfo(Repository repository, boolean newRepository) {
            this.repository = repository;
            this.newRepository = newRepository;
        }

        public RepositoryInfo(Repository repository) {
            this(repository, false);
        }

        public boolean isNewRepository() {
            return newRepository;
        }

        public Repository getRepository() {
            return repository;
        }
    }

    /**
     * Initialize the struts servlet.
     *
     * This routine creates the repository, adds the nodes namespaces and creates the RMI access.
     *
     * @param servlet
     * @param config
     * @throws ServletException
     */
    public void init(ActionServlet servlet, ModuleConfig config) throws ServletException {
        startup.info("Initializing repository at \"" + getRepositoryVersionPath() + "\"");
        try {
            RepositoryInfo repositoryInfo = createRepository(servlet.getServletContext());
            repository = repositoryInfo.getRepository();

            initSession = repository.login();

            registerNodesNamespaces(initSession);
            allowRMIAccess(repository);
            RepositoryExtensionsManager.startUp(initSession);
            if (repositoryInfo.isNewRepository())
                addDefaultRepositoryContent(servlet.getServletContext(), initSession);

            startup.info("Repository started");
        } catch (Exception e) {
            logger.error("Exception during initialization", e);
            throw new ServletException(e);
        }
    }

    /**
     * Return the parent of the ZIP file entry. Omit the first folder,
     * substitute special characters, that are not possible in file names
     * on some operating systems (hence they will not appear in the ZIP).
     * 
     * @param path
     * @return
     */
    protected String getZipFileParentPath(String path) {
        String parentPath = path.replaceAll("#", ":").substring(path.indexOf(Constants.PATH_SEPARATOR), path.lastIndexOf(Constants.PATH_SEPARATOR));
        if (parentPath.length() == 0)
            parentPath = "/";

        return parentPath;
    }

    /**
     * Add default content to a newly created repository.
     * 
     * @param context
     * @param session
     * @throws IOException
     * @throws RepositoryException
     */
    protected void addDefaultRepositoryContent(ServletContext context, Session session) throws IOException, RepositoryException {
        startup.info("Adding default content ...");

        InputStream in = context.getResourceAsStream(REPOSITORY_CONTENT_ZIP);
        ZipInputStream zip = new ZipInputStream(in);

        ZipEntry fileEntry = null;
        while ((fileEntry = zip.getNextEntry()) != null) {
            String name = fileEntry.getName().replaceAll("#", ":");
            int start = name.indexOf(Constants.PATH_SEPARATOR);
            int end = name.lastIndexOf(Constants.PATH_SEPARATOR);

            if (fileEntry.isDirectory()) {
                if (start == end)
                    continue;
                String path = name.substring(start + 1, name.lastIndexOf(Constants.PATH_SEPARATOR)); // relative path from root
                if (path.length() > 0 && !session.getRootNode().hasNode(path))
                    session.getRootNode().addNode(path);
            } else {
                String parentPath = name.substring(start, end); // absolute path from root, start with separator
                if (parentPath.length() == 0)
                    parentPath = "/";
                session.getWorkspace().importXML(parentPath, new UncloseableInputStream(zip), ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
            }
        }

        zip.close();
    }

    /**
     * Extract the repository configuration from the ZIP file that is part of the WAR.
     *
     * @return
     * @throws IOException
     */
    void extractRepository(ServletContext context) throws IOException {
        startup.info("Creating the repository ...");

        InputStream in = context.getResourceAsStream(REPOSITORY_ZIP);
        ZipInputStream zip = new ZipInputStream(in);

        ZipEntry fileEntry = null;
        while ((fileEntry = zip.getNextEntry()) != null) {
            if (fileEntry.isDirectory()) {
                File file = new File(getRepositoryHome() + Constants.PATH_SEPARATOR + fileEntry.getName().substring(0, fileEntry.getName().length() - 1));
                file.mkdirs();
            } else {
                File outFile = new File(getRepositoryHome() + Constants.PATH_SEPARATOR + fileEntry.getName());
                FileOutputStream out = new FileOutputStream(outFile);

                byte[] buf = new byte[8192];
                int count = 0;
                while ((count = zip.read(buf)) >= 0) {
                    out.write(buf, 0, count);
                }

                out.close();
            }
        }

        zip.close();
    }

    /**
     * Create the repository. If the configuration has already been extracted, use it.
     * Else extract the configuration and create the repository.
     *
     * @return
     * @throws IOException
     */
    protected RepositoryInfo createRepository(ServletContext context) throws IOException {
        File config = new File(getRepositoryConfigPath());
        if (config.exists())
            return new RepositoryInfo(new TransientRepository(getRepositoryConfigPath(), getRepositoryVersionPath()));

        extractRepository(context);

        return new RepositoryInfo(new TransientRepository(getRepositoryConfigPath(), getRepositoryVersionPath()), true);
    }

    /**
     * Make the repository be accessible via RMI
     * 
     * @param repository
     * @throws RemoteException
     * @throws AlreadyBoundException
     * @throws MalformedURLException
     */
    protected void allowRMIAccess(Repository repository) throws RemoteException, AlreadyBoundException, MalformedURLException {
        // Start the rmiregistry, if necessary
        try {
            Registry registry = LocateRegistry.getRegistry();
            registry.list();
        } catch (Exception e) {
            LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
        }

        RemoteAdapterFactory factory = new ServerAdapterFactory();
        RemoteRepository remote = factory.getRemoteRepository(repository);
        Naming.rebind(Constants.URL_RMI, remote);
        
        for (Registration rmiService : NodesConfig.getInstance().getRmiServices())
            rmiService.register(repository);
    }

    /**
     * Add nodes namespaces to the repository
     * 
     * @param session
     * @throws RepositoryException
     */
    protected void registerNodesNamespaces(Session session) throws RepositoryException {
        SimpleNamespaceRegistry nodesNamespaces = SimpleNamespaceRegistry.getInstance();
        String[] nodesPrefixes = nodesNamespaces.getPrefixes();

        Workspace workspace = session.getWorkspace();
        NamespaceRegistry repositoryNamespaces = workspace.getNamespaceRegistry();
        String[] repositoryPrefixes = repositoryNamespaces.getPrefixes();
        for (int i = 0; i < nodesPrefixes.length; i++) {
            boolean found = false;
            for (int j = 0; j < repositoryPrefixes.length; j++) {
                if (nodesPrefixes[i].equals(repositoryPrefixes[j])) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                repositoryNamespaces.registerNamespace(nodesPrefixes[i], nodesNamespaces.getURI(nodesPrefixes[i]));
                logger.info("Registered " + nodesPrefixes[i]);
            }
        }
    }

    @Override
    public void destroy() {
        logger.info("Repository shutdown");
        ((TransientRepository) repository).shutdown();
    }

    public String getRepositoryConfigPath() {
        return getRepositoryVersionPath() + File.separator + REPOSITORY_CONFIG_NAME;
    }

    public String getRepositoryVersionPath() {
        return getRepositoryHome() + File.separator + REPOSITORY_BASE;
    }

    public String getRepositoryHome() {
        // repository home may have been set in java, e.g. by the jetty launcher
        String home = System.getProperty(REPOSITORY_HOME);

        // else it may have been set in the environment
        if (home == null) 
            home = System.getenv(REPOSITORY_HOME);

        // specific case for tomcat
        if (home == null)
            home = System.getProperty(TOMCAT_HOME);

        // finally, use the home folder of the current user
        if (home == null)
            home = System.getProperty("user.home");
        
        return home + File.separator + APPLICATION_NAME;
    }

    class UncloseableInputStream extends InputStream {

        InputStream in;

        public UncloseableInputStream(InputStream in) {
            this.in = in;
        }

        public long skip(long n) throws IOException {
            return in.skip(n);
        }

        public synchronized void reset() throws IOException {
            in.reset();
        }

        public int read(byte[] b, int off, int len) throws IOException {
            return in.read(b, off, len);
        }

        public int read(byte[] b) throws IOException {
            return in.read(b);
        }

        public int read() throws IOException {
            return in.read();
        }

        public boolean markSupported() {
            return in.markSupported();
        }

        public synchronized void mark(int readlimit) {
            in.mark(readlimit);
        }

        public void close() throws IOException {
        }

        public int available() throws IOException {
            return in.available();
        }
    }
}
